본문으로 이동
메뉴 여닫기
환경 설정 메뉴 여닫기
개인 메뉴 여닫기
로그인하지 않음
지금 편집한다면 당신의 IP 주소가 공개될 수 있습니다.

스핀 락(Spin Lock)은 락을 얻지 못한 스레드가 잠들지 않고, 락이 풀릴 때까지 반복문을 돌며 계속 확인하는(바쁜 대기, busy waiting) 방식의 상호 배제 동기화 기법이다.

임계 구역에 하나의 실행 흐름만 들어가게 하는 락은, 락을 얻지 못했을 때 어떻게 기다리느냐에 따라 두 부류로 나뉜다. 뮤텍스나 세마포어는 기다리는 스레드를 잠재우고(sleep) 다른 스레드에게 CPU를 넘긴다. 스핀 락은 CPU를 놓지 않고 그 자리에서 '돌면서'(spin) 락 변수를 계속 확인한다.

잠들고 깨어나는 데는 문맥 교환과 스케줄러 호출 비용이 든다. 임계 구역이 아주 짧아서 금방 락이 풀릴 것이라면, 잠드는 비용보다 잠깐 도는 비용이 더 싸다. 또 운영체제 커널의 인터럽트 처리 코드처럼 잠들 수 없는 문맥에서는 스핀 락 말고는 쓸 수 있는 락이 없다. 그래서 스핀 락은 다중 프로세서 운영체제 커널의 가장 기본적인 동기화 수단이다.

여러 CPU가 동시에 락 변수를 확인하고 바꾸면 둘 다 락을 얻는 경쟁 상태가 생긴다. 그래서 '확인하고 바꾸기'를 쪼갤 수 없는 하나의 동작으로 하는 원자적 명령어를 쓴다.

  • Test-and-Set : 메모리 값을 1로 바꾸면서 이전 값을 돌려준다. 이전 값이 0이었으면 락을 얻은 것이다.
  • Compare-and-Swap(CAS) : 메모리 값이 기대한 값과 같을 때만 새 값으로 바꾼다.
  • Exchange(x86의 xchg), Load-Linked/Store-Conditional(ARM, RISC-V) 등
#include <stdatomic.h>
atomic_flag lock = ATOMIC_FLAG_INIT;

void spin_lock(void) {
    while (atomic_flag_test_and_set(&lock))   // 이전 값이 1이면 남이 쥐고 있음
        ;                                      // 풀릴 때까지 계속 돈다
}

void spin_unlock(void) {
    atomic_flag_clear(&lock);
}
  • TTAS(Test-and-Test-and-Set) : 원자적 쓰기를 반복하면 캐시 라인이 CPU 사이를 계속 오가며 무효화가 일어난다. 먼저 일반 읽기로 풀렸는지 확인하다가, 풀린 것이 보일 때만 원자적 명령을 시도해 버스 트래픽을 줄인다.
  • 백오프 : 실패하면 점점 길게 쉬었다 다시 시도한다. CPU의 pause 명령으로 전력과 파이프라인 낭비를 줄인다.
  • 티켓 스핀 락 : 번호표를 뽑고 차례가 오면 들어간다. 먼저 온 순서대로 락을 주어 기아(starvation)를 막는다.
  • MCS 락, 큐 스핀 락 : 대기자마다 자기 노드의 변수에서만 돌게 해, 대기자가 많아도 캐시 라인 경합이 생기지 않는다. 리눅스는 qspinlock을 쓴다.

뮤텍스·세마포어와 비교

편집 원본 편집
구분 스핀 락 뮤텍스 세마포어
대기 방식 바쁜 대기(CPU 점유) 잠들기(블로킹) 잠들기(블로킹)
문맥 교환 없음 있음 있음
동시 진입 수 1 1 카운터 값만큼
적합한 임계 구역 매우 짧은 구간 길거나 잠들 수 있는 구간 자원 개수 관리
인터럽트 문맥 사용 가능 불가(잠들 수 없음) 불가
단일 CPU에서 의미 없음(락 쥔 쪽이 실행될 수 없어 시간만 낭비) 유효 유효
소유권 보통 획득자가 해제 획득자만 해제 소유 개념 없음

적합한 경우와 주의점

편집 원본 편집
  • 임계 구역이 문맥 교환 두 번보다 짧을 때(몇 개의 변수 갱신, 리스트 조작)
  • CPU가 여럿이어서, 기다리는 동안 락을 쥔 스레드가 다른 CPU에서 실제로 실행되고 있을 때
  • 인터럽트 핸들러, 스케줄러 내부처럼 잠들 수 없는 문맥
  • 락을 쥔 채 잠들면 안 된다 : 메모리 할당 대기, 입출력, 사용자 공간 접근처럼 잠들 수 있는 함수를 부르면 다른 CPU들이 끝없이 돈다.
  • 인터럽트와의 교착 : 스레드가 락을 쥔 상태에서 같은 CPU에 인터럽트가 들어오고, 그 핸들러가 같은 락을 잡으려 하면 영원히 돈다(교착상태). 그래서 인터럽트와 공유하는 락은 인터럽트를 끄고 잡는다.
  • 선점 : 락을 쥔 스레드가 선점당하면 다른 CPU들이 헛돈다. 커널은 스핀 락을 잡는 동안 선점을 막는다.
  • 우선순위 역전과 기아 : 단순 스핀 락은 순서를 보장하지 않는다.
  • 재진입 불가 : 같은 스레드가 두 번 잡으면 스스로 교착된다.

읽기/쓰기 스핀 락

편집 원본 편집

읽기/쓰기 스핀 락(Read/Write Spin Lock, rwlock)은 읽기만 하는 쪽은 여럿이 동시에 들어가게 하고, 쓰는 쪽은 혼자만 들어가게 하는 스핀 락이다. 읽기가 쓰기보다 훨씬 많은 자료 구조에서 동시성을 높인다.

  • 읽기 락 : 쓰는 쪽이 없으면 읽는 쪽 수를 늘리고 바로 들어간다.
  • 쓰기 락 : 읽는 쪽도 쓰는 쪽도 없을 때까지 돈 뒤 혼자 들어간다.
  • 문제점 : 읽는 쪽이 끊임없이 들어오면 쓰는 쪽이 계속 기다리는 쓰기 기아가 생긴다. 또 읽기 락도 공유 카운터를 원자적으로 바꾸므로 CPU가 많으면 캐시 라인 경합이 크다.
  • 대안 : 쓰는 쪽을 우선하는 순차 락(seqlock), 읽는 쪽이 락 없이 읽는 RCU(Read-Copy-Update)가 많이 쓰인다.

리눅스 커널의 스핀 락

편집 원본 편집
API 용도
spin_lock() / spin_unlock() 프로세스 문맥끼리만 공유하는 데이터
spin_lock_irqsave() / spin_unlock_irqrestore() 하드웨어 인터럽트 핸들러와 공유하는 데이터. 로컬 인터럽트를 끄고 이전 상태를 저장
spin_lock_irq() / spin_unlock_irq() 인터럽트가 켜져 있다고 확신할 때 끄고 잡기
spin_lock_bh() / spin_unlock_bh() 소프트 IRQ·태스클릿 같은 하반부(bottom half)와 공유하는 데이터
read_lock() / write_lock() 읽기/쓰기 스핀 락(rwlock_t)
spin_trylock() 돌지 않고 한 번만 시도
  • 단일 프로세서 커널로 빌드하면 실제 회전 코드는 사라지고, 선점 금지(선점형 커널일 때)와 _irq 계열의 인터럽트 금지만 남는다.
  • 실시간 패치(PREEMPT_RT)를 적용하면 spinlock_t는 잠들 수 있는 rt_mutex 기반 락으로 바뀌고, 정말 돌아야 하는 곳은 raw_spinlock_t를 쓴다.
  • 디바이스 드라이버 : 네트워크 카드 드라이버의 송수신 링 버퍼는 인터럽트 핸들러(패킷 수신)와 일반 코드(패킷 송신)가 함께 건드린다. spin_lock_irqsave()로 보호한다.
  • 스케줄러 : CPU별 실행 큐(runqueue)를 조작할 때 짧게 스핀 락을 잡는다.
  • 메모리 할당기 : 페이지·슬랩 할당기의 프리 리스트 조작
  • 라우팅 테이블 : 패킷마다 읽고 가끔 갱신하는 테이블에 읽기/쓰기 락이나 RCU를 쓴다.
  • 사용자 공간 : 고성능 서버와 게임 엔진의 짧은 공유 큐. 대부분의 사용자 공간 뮤텍스(glibc의 적응형 뮤텍스)는 잠깐 돌다가 안 되면 잠드는 방식으로 둘의 장점을 섞는다.